﻿// Nova.Studio - a GUI test framework for the Nova.CodeDOM C# object model library.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;

using Nova.CodeDOM;
using Nova.UI;

namespace Nova.Studio
{
    /// <summary>
    /// Interaction logic for CodeWindow.xaml
    /// </summary>
    public partial class CodeWindow : UserControl
    {
        #region /* FIELDS */

        protected string _description;
        protected CodeObjectVM _codeVM;
        protected bool _textViewEnabled;
        protected double _windowHeight;
        protected double _verticalScrollOffset;
        protected double _fontHeight;
        protected bool _skipFirstScrollEvent = true;
        protected CodeRenderer _renderer;
        protected Dictionary<CodeObject, CodeObjectVM> _codeObject2VM = new Dictionary<CodeObject, CodeObjectVM>();

        // Save values for text view
        protected readonly GridLength _textColumnWidth;
        protected readonly Thickness _codeBorderMargin;

        #endregion

        #region /* CONSTRUCTORS */

        public CodeWindow(string description, CodeObjectVM codeVM)
        {
            InitializeComponent();
            BindCommands(Application.Current.MainWindow);
            _description = description;

            // Create a new CodeObjectVM tree, so that are changes are reflected and all VMs are added to the local dictionary
            _codeVM = CodeObjectVM.CreateVM(codeVM.CodeObject, codeVM.ParentVM, false, _codeObject2VM);

            _fontHeight = 15;  // Default - changes in Render()
            _textColumnWidth = mainGrid.ColumnDefinitions[1].Width;
            _codeBorderMargin = codeBorder.Margin;

            // WPF tooltips are an unbelievable nightmare!!  Any ToolTip set to null will inherit from the parent, and
            // doing ToolTipService.SetIsEnabled(control, false) has the SAME effect!  Stopping tooltip inheritance requires
            // both setting a non-null value, and then hacking the display so that it's off-screen!!!
            // THIS IS NO LONGER USED in favor of custom tooltips, and moved the TabItem tooltip to the Header to prevent it
            // from stupidly cascading to all controls on the tab.
            //mainGrid.ToolTip = "";
            //ToolTipService.SetVerticalOffset(mainGrid, int.MaxValue);  // Force off-screen to prevent display
        }

        #endregion

        #region /* PROPERTIES */

        public string Description
        {
            get { return _description; }
        }

        public CodeObjectVM CodeVM
        {
            get { return _codeVM; }
        }

        public bool TextViewEnabled
        {
            get { return _textViewEnabled; }
            set { _textViewEnabled = value; }
        }

        #endregion

        #region /* METHODS */

        protected static readonly GridLength ZeroGridLength = new GridLength(0);
        protected static readonly Thickness ZeroThickness = new Thickness(0);

        /// <summary>
        /// Render the code contained within this editor object.
        /// </summary>
        public void Render(double windowHeight)
        {
            using (new WaitCursor())
            {
                _windowHeight = windowHeight;
                if (_codeVM != null)
                {
                    // Reset statistics
                    Stopwatch stopWatch = new Stopwatch();
                    stopWatch.Start();

                    // Populate types combobox
                    if (_codeVM is CodeUnitVM)
                        PopulateTypesComboBox(((CodeUnitVM)_codeVM).CodeUnit);

                    // Measure and render the code into the panel
                    codePanel.Children.Clear();
                    _renderer = _codeVM.Render(codePanel, _verticalScrollOffset, _verticalScrollOffset + _windowHeight);
                    _renderer.ToolTipContextMenuOpening = codeViewer_ContextMenuOpening;
                    _renderer.CommandBindings = Application.Current.MainWindow.CommandBindings;
                    _fontHeight = CodeRenderer.CodeFontHeight;

                    // Add "end-of-file" marker
                    TextBlock textBlock = new TextBlock
                        {
                            Text = "",
                            Foreground = Brushes.Black,
                            ToolTip = "End of File",
                            FontFamily = CodeRenderer.CodeFontFamily,
                            FontSize = CodeRenderer.CodeFontSize
                        };
                    codePanel.Children.Add(textBlock);

                    if (!CodeRenderer.VirtualizeRendering)
                    {
                        // Log statistics
                        string statistics = "Render '" + _description + "', elapsed time: " + stopWatch.Elapsed.TotalSeconds.ToString("N3");
                        if (_renderer != null)
                        {
                            statistics += ", TextBlocks=" + _renderer.TextBlockCount + ", WrapPanels=" + _renderer.WrapPanelCount
                                          + ", StackPanels=" + _renderer.StackPanelCount + ", Borders=" + _renderer.BorderCount;
                        }
                        Log.WriteLine(statistics);
                    }
                }

                // Render the text view
                RenderTextView();
            }
        }

        /// <summary>
        /// Render the text view of the code.
        /// </summary>
        public void RenderTextView()
        {
            if (_textViewEnabled)
            {
                Stopwatch stopWatch = new Stopwatch();
                stopWatch.Start();
                textView.Text = (_codeVM != null ? _codeVM.CodeObject.AsText() : null);
                textView.FontFamily = CodeRenderer.CodeFontFamily;
                textView.FontSize = CodeRenderer.CodeFontSize;
                codeBorder.Margin = _codeBorderMargin;
                mainGridSplitter.Visibility = Visibility.Visible;
                mainGrid.ColumnDefinitions[1].Width = _textColumnWidth;
                Log.WriteLine("Text Render '" + _description + "', elapsed time: " + stopWatch.Elapsed.TotalSeconds.ToString("N3"));
            }
            else
            {
                textView.Text = null;
                codeBorder.Margin = ZeroThickness;
                mainGridSplitter.Visibility = Visibility.Collapsed;
                mainGrid.ColumnDefinitions[1].Width = ZeroGridLength;
            }
        }

        /// <summary>
        /// Select the specified code object within the editor (scroll until it's visible if necessary, and highlight it).
        /// </summary>
        public void SelectCodeObject(CodeObject codeObject)
        {
            if (_renderer == null) return;
            _renderer.CloseToolTip();

            // Get the corresponding VM for the code object
            CodeObjectVM codeObjectVM;
            if (_codeObject2VM.TryGetValue(codeObject, out codeObjectVM))
            {
                // Do a layout so that the dimensions are updated
                codeScrollViewer.UpdateLayout();

                // Scroll the VM into view if necessary so it will be rendered
                double newScrollOffset = -1;
                if (codeObjectVM is CodeUnitVM)
                {
                    if (codeScrollViewer.VerticalOffset > 0)
                        newScrollOffset = 0;
                }
                else
                {
                    const double margin = 10;
                    double absoluteY = codeObjectVM.GetAbsoluteY();
                    if (absoluteY < codeScrollViewer.VerticalOffset + margin)
                    {
                        newScrollOffset = absoluteY - margin;
                        if (newScrollOffset < 0)
                            newScrollOffset = 0;
                    }
                    else if (absoluteY > codeScrollViewer.VerticalOffset + codeScrollViewer.ViewportHeight - margin - _fontHeight)
                        newScrollOffset = absoluteY - (codeScrollViewer.ViewportHeight - margin - _fontHeight);
                }
                if (newScrollOffset >= 0)
                {
                    codeScrollViewer.ScrollToVerticalOffset(newScrollOffset);
                    _skipFirstScrollEvent = false;

                    // Process pending events from the scroll so that UI objects are generated before accessing them below
                    WPFUtil.DoEvents();
                }

                // Highlight the primary UI object of the VM with an animation (skip if it's a CodeUnit)
                if (!(codeObjectVM is CodeUnitVM))
                {
                    if (codeObjectVM.FrameworkElement != null)
                    {
                        FrameworkElement frameworkElement = codeObjectVM.GetSelectionElement();

                        // Bring into view (horizontal scrolling might be required)
                        frameworkElement.BringIntoView();

                        // Create a centered scale transform for the element
                        ScaleTransform scaleTransform = new ScaleTransform();
                        frameworkElement.RenderTransform = scaleTransform;
                        frameworkElement.RenderTransformOrigin = new Point(0.5, 0.5);

                        // Create scaling animations, starting at 2X for Y so the layout gives extra room from the edge of the view.
                        // Repeat 2.5 times, so it ends at 1X.  Scale Y more than X, because scaling X too much can send text behind
                        // following text that has a higher Z order.
                        DoubleAnimation animationX = new DoubleAnimation(1.2, 1.0, new Duration(new TimeSpan(0, 0, 0, 0, 200)))
                            { AutoReverse = true, RepeatBehavior = new RepeatBehavior(2.5), FillBehavior = FillBehavior.Stop };
                        DoubleAnimation animationY = new DoubleAnimation(2.0, 1.0, new Duration(new TimeSpan(0, 0, 0, 0, 200)))
                            { AutoReverse = true, RepeatBehavior = new RepeatBehavior(2.5), FillBehavior = FillBehavior.Stop };
                        scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, animationX);
                        scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, animationY);
                    }
                    else
                    {
                        // If no UI element was found, it's probably a hidden VM, such as the VarTypeRefVM for a ParameterDeclVM with
                        // an inferred type (the VM is still generated because it's used to determine the color of the parent text), so
                        // recursively call this routine for the parent VM to display that instead.
                        SelectCodeObject(codeObject.Parent);
                    }
                }
            }
        }

        public void ResetToolTipTimer()
        {
            _renderer.ResetToolTipTimer();
        }

        public void CloseToolTip()
        {
            _renderer.CloseToolTip();
        }

        #endregion

        #region /* EVENTS */

        protected void codeViewer_ContextMenuOpening(object sender, ContextMenuEventArgs e)
        {
            bool isToolTipContextMenu = sender is Popup;
            if (!isToolTipContextMenu)
                _renderer.CloseToolTip();

            ContextMenu contextMenu = new ContextMenu();

            // Add context specific menu items here
            object source = e.Source;
            if (source is Run)
                source = ((Run)source).Parent;
            FrameworkElement frameworkElement = source as FrameworkElement;
            if (frameworkElement != null)
            {
                CodeObject target = GetReferenceTarget(frameworkElement);
                if (target != null)
                {
                    object targetObject = (target is SymbolicRef ? ((SymbolicRef)target).Reference : target);
                    if (targetObject is CodeObject && !(targetObject is Namespace) && (target is SymbolicRef || isToolTipContextMenu))
                        WPFUtil.AddMenuItemCommand(contextMenu, GoToDeclarationCommand, frameworkElement);
                }
            }

            // Add general menu items:
            if (!isToolTipContextMenu)
            {
                WPFUtil.AddMenuItemCommand(contextMenu, ApplicationCommands.Close, this);
                WPFUtil.AddMenuItemCommand(contextMenu, ApplicationCommands.Save, this);
                WPFUtil.AddSeparator(contextMenu);
                WPFUtil.AddCheckableMenuItemCommand(contextMenu, TextViewCommand, this, _textViewEnabled);

                MenuItem optionsMenu = new MenuItem { Header = "Display Options" };
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, CodeRenderer.ShowBordersCommand, this, CodeRenderer.ShowBorders);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, CodeRenderer.ShowBackgroundColorsCommand, this, CodeRenderer.ShowBackgroundColors);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, CodeRenderer.UseShadingCommand, this, CodeRenderer.UseShading);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, CodeRenderer.HalfHeightBlankLinesCommand, this, CodeRenderer.HalfHeightBlankLines);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, BlockVM.TinyBracesCommand, this, BlockVM.TinyBraces);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, BlockVM.HideBracesCommand, this, BlockVM.HideBraces);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, StatementVM.HideTerminatorsCommand, this, StatementVM.HideTerminators);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, StatementVM.HideStatementParensCommand, this, StatementVM.HideStatementParens);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, MethodDeclBaseVM.HideMethodParensCommand, this, MethodDeclBaseVM.HideMethodParens);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, NewObjectVM.HideEmptyParensCommand, this, NewObjectVM.HideEmptyParens);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, WhileVM.DisplayAsDoIfNullConditionCommand, this, WhileVM.DisplayAsDoIfNullCondition);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, OperatorVM.AlternativeDisplayCommand, this, OperatorVM.UseAlternativeDisplay);
                WPFUtil.AddCheckableMenuItemCommand(optionsMenu, CodeRenderer.MaximizeBordersCommand, this, CodeRenderer.MaximizeBorders);
                {
                    MenuItem commentsMenu = new MenuItem { Header = "Comments" };
                    WPFUtil.AddCheckableMenuItemCommand(commentsMenu, CommentBaseVM.HideDelimitersCommand, this, CommentBaseVM.HideDelimiters);
                    WPFUtil.AddCheckableMenuItemCommand(commentsMenu, CommentBaseVM.HideAllCommand, this, CommentBaseVM.HideAll);
                    WPFUtil.AddCheckableMenuItemCommand(commentsMenu, CommentVM.UseProportionalFontCommand, this, CommentVM.UseProportionalFont);
                    WPFUtil.AddCheckableMenuItemCommand(commentsMenu, DocCommentVM.UseProportionalFontCommand, this, DocCommentVM.UseProportionalFont);
                    WPFUtil.AddCheckableMenuItemCommand(commentsMenu, DocCommentVM.AlternativeFormatCommand, this, DocCommentVM.UseAlternativeFormat);
                    optionsMenu.Items.Add(commentsMenu);

                    MenuItem literalsMenu = new MenuItem { Header = "Literals" };
                    WPFUtil.AddCheckableMenuItemCommand(literalsMenu, LiteralVM.CommasInNumericsCommand, this, LiteralVM.CommasInNumerics);
                    WPFUtil.AddCheckableMenuItemCommand(literalsMenu, LiteralVM.NoEscapesCommand, this, LiteralVM.NoEscapes);
                    WPFUtil.AddCheckableMenuItemCommand(literalsMenu, LiteralVM.VisibleSpacesCommand, this, LiteralVM.VisibleSpaces);
                    WPFUtil.AddCheckableMenuItemCommand(literalsMenu, LiteralVM.HideQuotesCommand, this, LiteralVM.HideQuotes);
                    optionsMenu.Items.Add(literalsMenu);
                }
                contextMenu.Items.Add(optionsMenu);
            }

            contextMenu.IsOpen = contextMenu.HasItems;
            ((FrameworkElement)sender).ContextMenu = contextMenu;
            e.Handled = true;
        }

        protected static CodeObject GetReferenceTarget(FrameworkElement frameworkElement)
        {
            // Get the object associated with the FrameworkElement, handling hidden refs
            CodeObjectVM targetVM = frameworkElement.Tag as CodeObjectVM;
            if (targetVM != null)
            {
                CodeObject codeObject = targetVM.CodeObject;
                if (!(codeObject is INamedCodeObject || codeObject is SymbolicRef))
                    codeObject = codeObject.HiddenRef;
                return codeObject;
            }
            return null;
        }

        protected void textView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
        {
            ContextMenu contextMenu = new ContextMenu { IsOpen = true };

            // Add context specific menu items here

            // Add general menu items
            WPFUtil.AddMenuItemCommand(contextMenu, ApplicationCommands.Copy, this);
            WPFUtil.AddSeparator(contextMenu);
            WPFUtil.AddCheckableMenuItemCommand(contextMenu, TextViewCommand, this, _textViewEnabled);

            ((FrameworkElement)sender).ContextMenu = contextMenu;
            e.Handled = true;
        }

        protected void codeViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            _renderer.CloseToolTip();
            _windowHeight = e.ViewportHeight;
            _verticalScrollOffset = e.VerticalOffset;

            // Skip the first scroll event, since all necessary rendering will have already been done.
            // Otherwise, render any newly visible objects (and free those that are no longer visible).
            if (CodeRenderer.VirtualizeRendering && !_skipFirstScrollEvent)
            {
                _renderer.StartY = _verticalScrollOffset;
                _renderer.EndY = _verticalScrollOffset + _windowHeight;
                try
                {
                    _codeVM.RenderVisible(_renderer, CodeObjectVM.RenderFlags.None);
                }
                catch { }
            }

            _skipFirstScrollEvent = false;
            e.Handled = true;
        }

        private void UserControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (!IsVisible)
                _renderer.CloseToolTip();
        }

        #endregion

        #region /* TYPES/MEMBERS COMBOBOXES */

        protected void PopulateTypesComboBox(CodeUnit codeUnit)
        {
            List<TypeDeclWrapper> typeDeclWrappers = new List<TypeDeclWrapper>();
            foreach (TypeDecl typeDecl in codeUnit.GetTypeDecls(true, true))
                typeDeclWrappers.Add(new TypeDeclWrapper(typeDecl));
            typeDeclWrappers.Sort();
            typesComboBox.ItemsSource = typeDeclWrappers;
            if (typeDeclWrappers.Count == 1)
                typesComboBox.SelectedIndex = 0;
        }

        protected void PopulateMembersComboBox(TypeDeclWrapper typeDeclWrapper)
        {
            List<NamedCodeObjectWrapper> namedCodeObjectWrappers = new List<NamedCodeObjectWrapper>();
            foreach (INamedCodeObject member in typeDeclWrapper.TypeDecl.GetMemberDecls(true))
                namedCodeObjectWrappers.Add(new NamedCodeObjectWrapper(member, typeDeclWrapper.Name));
            namedCodeObjectWrappers.Sort();
            membersComboBox.ItemsSource = namedCodeObjectWrappers;
        }

        private void typesComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            TypeDeclWrapper typeDeclWrapper = typesComboBox.SelectedItem as TypeDeclWrapper;
            if (typeDeclWrapper != null)
            {
                SelectCodeObject(typeDeclWrapper.TypeDecl);
                PopulateMembersComboBox(typeDeclWrapper);
            }
        }

        private void membersComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            NamedCodeObjectWrapper namedCodeObjectWrapper = membersComboBox.SelectedItem as NamedCodeObjectWrapper;
            if (namedCodeObjectWrapper != null)
                SelectCodeObject(namedCodeObjectWrapper.NamedCodeObject as CodeObject);
        }

        protected class TypeDeclWrapper : IComparable<TypeDeclWrapper>
        {
            public TypeDecl TypeDecl;
            public string Name;
            public TypeDeclWrapper(TypeDecl typeDecl)
            {
                TypeDecl = typeDecl;
                Name = typeDecl.GetFullName(true);
            }
            public int CompareTo(TypeDeclWrapper other) { return string.Compare(Name, other.Name); }
            public override string ToString() { return Name; }
        }

        protected class NamedCodeObjectWrapper : IComparable<NamedCodeObjectWrapper>
        {
            public INamedCodeObject NamedCodeObject;
            public string Name;
            public NamedCodeObjectWrapper(INamedCodeObject namedCodeObject, string parentTypeName)
            {
                NamedCodeObject = namedCodeObject;
                Name = namedCodeObject.GetFullName(true).Substring(parentTypeName.Length + 1);
            }
            public int CompareTo(NamedCodeObjectWrapper other) { return string.Compare(Name, other.Name); }
            public override string ToString() { return Name; }
        }

        #endregion

        #region /* COMMANDS */

        public static readonly RoutedCommand GoToDeclarationCommand = new RoutedCommand("Go To Declaration", typeof(CodeWindow));
        public static readonly RoutedCommand TextViewCommand = new RoutedCommand("Text View", typeof(CodeWindow));

        public void BindCommands(Window window)
        {
            WPFUtil.AddCommandBinding(window, GoToDeclarationCommand, goToDeclaration_Executed);
            WPFUtil.AddCommandBinding(window, TextViewCommand, textView_Executed);
        }

        protected static void goToDeclaration_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MainWindow mainWindow = (MainWindow)Application.Current.MainWindow;
            FrameworkElement frameworkElement = e.OriginalSource as FrameworkElement;
            if (frameworkElement != null)
            {
                // Get the target object, and navigate to it (or the referenced object if it's a SymbolicRef)
                CodeObject targetCodeObject = GetReferenceTarget(frameworkElement);
                CodeObject codeObject = (targetCodeObject is SymbolicRef ? ((SymbolicRef)targetCodeObject).Reference as CodeObject : targetCodeObject);
                mainWindow.SelectCodeObject(codeObject);
            }
        }

        protected static void textView_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            CodeWindow codeWindow = (CodeWindow)e.Source;
            codeWindow._textViewEnabled = !codeWindow._textViewEnabled;
            codeWindow.RenderTextView();
            e.Handled = true;
        }

        #endregion
    }
}
